iT邦幫忙

2025 iThome 鐵人賽

DAY 20
0
Modern Web

我與型別的 30 天約定:TypeScript 入坑實錄系列 第 20

Day 20|錯誤處理與例外型別化:用型別守住你的錯誤流

  • 分享至 

  • xImage
  •  

在 TypeScript 專案裡,錯誤處理常常是最鬆的地方:

  • API 回傳錯誤格式不一致
  • 前端 catch 到的 err 永遠是 any
  • 後端丟 throw,前端只好亂猜內容

今天我們用三招,把錯誤處理變得 可預期、可補全、可檢查


1) 後端:統一錯誤格式

假設後端用 Express(Day 15–18 範例),我們先定義一個錯誤回應型別:

// src/types/api.ts
export type ApiError = {
  code: string;       // 錯誤代碼,例如 "USER_NOT_FOUND"
  message: string;    // 錯誤描述
  details?: unknown;  // 可選,放更多資訊
};

在 Express 中統一處理:

// src/middleware/errorHandler.ts
import { Request, Response, NextFunction } from "express";
import type { ApiError } from "../types/api";

export function errorHandler(err: any, req: Request, res: Response, next: NextFunction) {
  console.error(err);

  const apiError: ApiError = {
    code: err.code || "INTERNAL_ERROR",
    message: err.message || "Something went wrong",
    details: err.details,
  };

  res.status(err.status || 500).json(apiError);
}

在路由中用 throw

router.get("/:id", async (req, res, next) => {
  try {
    const user = await prisma.user.findUnique({ where: { id: req.params.id } });
    if (!user) {
      const err = new Error("User not found");
      (err as any).code = "USER_NOT_FOUND";
      (err as any).status = 404;
      throw err;
    }
    res.json(user);
  } catch (e) {
    next(e);
  }
});

2) Result 型別模式(推薦)

Result 讓成功與失敗用 同一型別 表示:

// src/types/result.ts
export type Result<T, E> =
  | { ok: true; value: T }
  | { ok: false; error: E };

後端 Service 層:

import type { Result } from "../types/result";
import type { ApiError } from "../types/api";
import { prisma } from "../lib/prisma";
import type { User } from "@prisma/client";

export async function getUser(id: string): Promise<Result<User, ApiError>> {
  const user = await prisma.user.findUnique({ where: { id } });
  if (!user) {
    return {
      ok: false,
      error: { code: "USER_NOT_FOUND", message: "User not found" },
    };
  }
  return { ok: true, value: user };
}

好處:

  • 呼叫方不用 try/catch,直接用 if (res.ok) 分支
  • 型別提示成功/失敗時可用的欄位

3) 前端:型別安全的錯誤捕捉

假設 API 錯誤統一為 ApiError 格式,前端就可以這樣處理:

import type { ApiError } from "../types/api";
import type { User } from "../types/user";

async function fetchUser(id: string): Promise<User | ApiError> {
  const res = await fetch(`/users/${id}`);
  if (!res.ok) {
    return (await res.json()) as ApiError;
  }
  return (await res.json()) as User;
}

async function showUser(id: string) {
  const result = await fetchUser(id);
  if ("code" in result) {
    // result 是 ApiError
    console.error(result.code, result.message);
  } else {
    // result 是 User
    console.log(result.name);
  }
}

4) zod 驗證 API 回應

避免 API 格式被後端或網路異常搞壞,可以用 zod 在前端驗證:

import { z } from "zod";

const userSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string().email(),
});

const apiErrorSchema = z.object({
  code: z.string(),
  message: z.string(),
  details: z.any().optional(),
});

async function fetchUserSafe(id: string) {
  const res = await fetch(`/users/${id}`);
  const json = await res.json();

  if (!res.ok) {
    return apiErrorSchema.parse(json); // 失敗就丟錯
  }
  return userSchema.parse(json);
}

這樣:

  • 成功回應 會是 User 型別,且已驗證
  • 錯誤回應 一樣有型別與驗證

5) 使用 Result + zod 終極型別安全

type UserResult = Result<
  z.infer<typeof userSchema>,
  z.infer<typeof apiErrorSchema>
>;

async function fetchUserResult(id: string): Promise<UserResult> {
  const res = await fetch(`/users/${id}`);
  const json = await res.json();

  if (!res.ok) {
    return { ok: false, error: apiErrorSchema.parse(json) };
  }
  return { ok: true, value: userSchema.parse(json) };
}

// 呼叫方:
const result = await fetchUserResult("abc");
if (result.ok) {
  console.log(result.value.name);
} else {
  console.error(result.error.code);
}

6) 常見錯誤處理型別化技巧

  1. Error Class 型別化

    class AppError extends Error {
      constructor(
        public code: string,
        public status: number,
        public details?: unknown
      ) {
        super(code);
      }
    }
    
  2. 錯誤碼列舉

    export enum ErrorCode {
      UserNotFound = "USER_NOT_FOUND",
      EmailExists = "EMAIL_EXISTS",
    }
    
  3. HTTP 錯誤碼與型別對應

    type HttpStatus = 400 | 401 | 403 | 404 | 500;
    

上一篇
Day 19|React + TypeScript:Props / State / 事件 / Hook / API 串接一次搞懂
下一篇
Day 21|TypeScript 工具型別實戰:Pick / Omit / Partial / Required / Record / ReturnType 全解
系列文
我與型別的 30 天約定:TypeScript 入坑實錄24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言